# -*- coding: UTF-8 -*-
# By Quentin HARDY (quentin.hardy@protonmail.com) - bobsecq
# Big thanks to https://raw.githubusercontent.com/openSUSE/salt/189a19b6e8d28cc49e5ad5f2a683e1dfdce66a86/salt/modules/win_task.py

'''
Windows Task Scheduler Module

A module for working with the Windows Task Scheduler.
You can add and edit existing tasks.
You can add and clear triggers and actions.
You can list all tasks, folders, triggers, and actions.

Use COM & ITaskService

https://docs.microsoft.com/is-is/windows/win32/taskschd/task-scheduler-objects
'''
# Import Python libs
import logging
import time
from datetime import datetime
from datetime import timedelta
import pythoncom
import win32com.client
from utils import *

# Define Constants
# TASK_ACTION_TYPE
TASK_ACTION_EXEC = 0
TASK_ACTION_COM_HANDLER = 5
TASK_ACTION_SEND_EMAIL = 6
TASK_ACTION_SHOW_MESSAGE = 7

# TASK_COMPATIBILITY
TASK_COMPATIBILITY_AT = 0
TASK_COMPATIBILITY_V1 = 1
TASK_COMPATIBILITY_V2 = 2
TASK_COMPATIBILITY_V3 = 3

# TASK_CREATION
TASK_VALIDATE_ONLY = 0x1
TASK_CREATE = 0x2
TASK_UPDATE = 0x4
TASK_CREATE_OR_UPDATE = 0x6
TASK_DISABLE = 0x8
TASK_DONT_ADD_PRINCIPAL_ACE = 0x10
TASK_IGNORE_REGISTRATION_TRIGGERS = 0x20

# TASK_INSTANCES_POLICY
TASK_INSTANCES_PARALLEL = 0
TASK_INSTANCES_QUEUE = 1
TASK_INSTANCES_IGNORE_NEW = 2
TASK_INSTANCES_STOP_EXISTING = 3

# TASK_LOGON_TYPE
TASK_LOGON_NONE = 0
TASK_LOGON_PASSWORD = 1
TASK_LOGON_S4U = 2
TASK_LOGON_INTERACTIVE_TOKEN = 3
TASK_LOGON_GROUP = 4
TASK_LOGON_SERVICE_ACCOUNT = 5
TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6

# TASK_RUNLEVEL_TYPE
TASK_RUNLEVEL_LUA = 0
TASK_RUNLEVEL_HIGHEST = 1

# TASK_STATE_TYPE
TASK_STATE_UNKNOWN = 0
TASK_STATE_DISABLED = 1
TASK_STATE_QUEUED = 2
TASK_STATE_READY = 3
TASK_STATE_RUNNING = 4

# TASK_TRIGGER_TYPE
TASK_TRIGGER_EVENT = 0
TASK_TRIGGER_TIME = 1
TASK_TRIGGER_DAILY = 2
TASK_TRIGGER_WEEKLY = 3
TASK_TRIGGER_MONTHLY = 4
TASK_TRIGGER_MONTHLYDOW = 5
TASK_TRIGGER_IDLE = 6
TASK_TRIGGER_REGISTRATION = 7
TASK_TRIGGER_BOOT = 8
TASK_TRIGGER_LOGON = 9
TASK_TRIGGER_SESSION_STATE_CHANGE = 11

duration = {'Immediately': 'PT0M',
            'Indefinitely': 'PT0M',
            'Do not wait': 'PT0M',
            '15 seconds': 'PT15S',
            '30 seconds': 'PT30S',
            '1 minute': 'PT1M',
            '5 minutes': 'PT5M',
            '10 minutes': 'PT10M',
            '15 minutes': 'PT15M',
            '30 minutes': 'PT30M',
            '1 hour': 'PT1H',
            '2 hours': 'PT2H',
            '4 hours': 'PT4H',
            '8 hours': 'PT8H',
            '12 hours': 'PT12H',
            '1 day': ['P1D', 'PT24H'],
            '3 days': ['P3D', 'PT72H'],
            '30 days': 'P30D',
            '90 days': 'P90D',
            '180 days': 'P180D',
            '365 days': 'P365D'}

action_types = {'Execute': TASK_ACTION_EXEC,
                'Email': TASK_ACTION_SEND_EMAIL,
                'Message': TASK_ACTION_SHOW_MESSAGE}

trigger_types = {'Event': TASK_TRIGGER_EVENT,
                 'Once': TASK_TRIGGER_TIME,
                 'Daily': TASK_TRIGGER_DAILY,
                 'Weekly': TASK_TRIGGER_WEEKLY,
                 'Monthly': TASK_TRIGGER_MONTHLY,
                 'MonthlyDay': TASK_TRIGGER_MONTHLYDOW,
                 'OnIdle': TASK_TRIGGER_IDLE,
                 'OnTaskCreation': TASK_TRIGGER_REGISTRATION,
                 'OnBoot': TASK_TRIGGER_BOOT,
                 'OnLogon': TASK_TRIGGER_LOGON,
                 'OnSessionChange': TASK_TRIGGER_SESSION_STATE_CHANGE}

states = {TASK_STATE_UNKNOWN: 'Unknown',
          TASK_STATE_DISABLED: 'Disabled',
          TASK_STATE_QUEUED: 'Queued',
          TASK_STATE_READY: 'Ready',
          TASK_STATE_RUNNING: 'Running'}

instances = {'Parallel': TASK_INSTANCES_PARALLEL,
             'Queue': TASK_INSTANCES_QUEUE,
             'No New Instance': TASK_INSTANCES_IGNORE_NEW,
             'Stop Existing': TASK_INSTANCES_STOP_EXISTING}

results = {0x0: 'The operation completed successfully',
           0x1: 'Incorrect or unknown function called',
           0x2: 'File not found',
           0xA: 'The environment is incorrect',
           0x41300: 'Task is ready to run at its next scheduled time',
           0x41301: 'Task is currently running',
           0x41302: 'Task is disabled',
           0x41303: 'Task has not yet run',
           0x41304: 'There are no more runs scheduled for this task',
           0x41306: 'Task was terminated by the user',
           0x8004130F: 'Credentials became corrupted',
           0x8004131F: 'An instance of this task is already running',
           0x800704DD: 'The service is not available (Run only when logged in?)',
           0x800710E0: 'The operator or administrator has refused the request',
           0xC000013A: 'The application terminated as a result of CTRL+C',
           0xC06D007E: 'Unknown software exception'}

def _get_date_time_format(dt_string):
    '''
    Copied from win_system.py (_get_date_time_format)

    Function that detects the date/time format for the string passed.

    :param str dt_string:
        A date/time string

    :return: The format of the passed dt_string
    :rtype: str
    '''
    valid_formats = [
        '%I:%M:%S %p',
        '%I:%M %p',
        '%H:%M:%S',
        '%H:%M',
        '%Y-%m-%d',
        '%m-%d-%y',
        '%m-%d-%Y',
        '%m/%d/%y',
        '%m/%d/%Y',
        '%Y/%m/%d'
    ]
    for dt_format in valid_formats:
        try:
            datetime.strptime(dt_string, dt_format)
            return dt_format
        except ValueError:
            continue
    return False


def _get_date_value(date):
    '''
    Function for dealing with PyTime values with invalid dates. ie: 12/30/1899
    which is the windows task scheduler value for Never

    :param obj date: A PyTime object

    :return: A string value representing the date or the word "Never" for
    invalid date strings
    :rtype: str
    '''
    try:
        return '{0}'.format(date)
    except ValueError:
        return 'Never'


def _reverse_lookup(dictionary, value):
    '''
    Lookup the key in a dictionary by it's value. Will return the first match.

    :param dict dictionary: The dictionary to search

    :param str value: The value to search for.

    :return: Returns the first key to match the value
    :rtype: str
    '''
    value_index = -1
    for idx, dict_value in enumerate(dictionary.values()):
        if type(dict_value) == list:
            if value in dict_value:
                value_index = idx
                break
        elif value == dict_value:
            value_index = idx
            break

    return list(dictionary)[value_index]


def _lookup_first(dictionary, key):
    '''
    Lookup the first value given a key. Returns the first value if the key
    refers to a list or the value itself.

    :param dict dictionary: The dictionary to search

    :param str key: The key to get

    :return: Returns the first value available for the key
    :rtype: str
    '''
    value = dictionary[key]
    if type(value) == list:
        return value[0]
    else:
        return value


def _save_task_definition(name,
                          task_folder,
                          task_definition,
                          user_name,
                          password,
                          logon_type,
                          flags=TASK_CREATE_OR_UPDATE):
    '''
    Internal function to save the task definition.

    :param str name: The name of the task. If this value is Nothing, the task will be registered in the root task folder
                     and the task name will be a GUID value that is created by the Task Scheduler service.
    :param str task_folder: The object representing the folder in which to save the task
    :param str task_definition: The object representing the task to be saved
    :param str user_name: The user_account under which to run the task. If present, these credentials take priority over
                          the credentials specified in the task definition object pointed to by the definition parameter.
    :param str password: The password that corresponds to the user account
    :param int logon_type: The logon type for the task: TASK_LOGON_NONE, TASK_LOGON_PASSWORD, etc
    :param int flags: A TASK_CREATION constant: TASK_CREATE, TASK_UPDATE, TASK_CREATE_OR_UPDATE, etc
    :return: True if successful, False if not
    :rtype: bool
    '''
    logging.debug("Creating/registring/updating the task {0} as {1} using the defined TaskDefinition...".format(repr(name), repr(user_name)))
    logging.debug("TaskDefinition: logon_type={0}, user_name={1}, password={2}".format(logon_type, repr(user_name), repr(password)))
    try:
        task_folder.RegisterTaskDefinition(name,
                                           task_definition,
                                           flags,
                                           user_name,
                                           password,
                                           logon_type)
        logging.debug("The task {0} is registered or updated successfully".format(repr(name)))
        return True

    except pythoncom.com_error as error:
        #https://knowledge.broadcom.com/external/article/152212/error-codes-list-for-microsoft-technolog.html
        hr, msg, exc, arg = error.args  # pylint: disable=W0633
        fc = {-2147024773: 'The filename, directory name, or volume label syntax is incorrect',
              -2147024894: 'The system cannot find the file specified',
              -2147024891: 'Access denied',
              -2147216615: 'Required element or attribute missing',
              -2147216616: 'Value incorrectly formatted or out of range',
              -2147352571: 'Type mismatch'}
        try:
            failure_code = fc[exc[5]]
        except KeyError:
            failure_code = 'Unknown Failure: {0}'.format(error)

        logging.error('Failed to create/register/modify task {0}: {1}'.format(task_definition, failure_code))

        return 'Failed to modify task: {0}'.format(repr(failure_code))


def list_tasks(location='\\'):
    r'''
    List all tasks located in a specific location in the task scheduler.

    :param str location: A string value representing the folder from which you
        want to list tasks. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).

    :return: Returns a list of tasks.
    :rtype: list
    '''
    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the folder to list tasks from
    task_folder = task_service.GetFolder(location)
    tasks = task_folder.GetTasks(0)

    ret = []
    for task in tasks:
        ret.append(task.Name)

    return ret


def list_folders(location='\\'):
    r'''
    List all folders located in a specific location in the task scheduler.

    :param str location: A string value representing the folder from which you
        want to list tasks. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).

    :return: Returns a list of folders.
    :rtype: list

    CLI Example:

    .. code-block:: bash

        salt 'minion-id' task.list_folders
    '''
    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the folder to list folders from
    task_folder = task_service.GetFolder(location)
    folders = task_folder.GetFolders(0)

    ret = []
    for folder in folders:
        ret.append(folder.Name)

    return ret


def list_triggers(name, location='\\'):
    r'''
    List all triggers that pertain to a task in the specified location.

    :param str name: The name of the task for which list triggers.

    :param str location: A string value representing the location of the task
        from which to list triggers. Default is '\\' which is the root for the
        task scheduler (C:\Windows\System32\tasks).

    :return: Returns a list of triggers.
    :rtype: list

    CLI Example:

    .. code-block:: bash

        salt 'minion-id' task.list_triggers <task_name>
    '''
    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the folder to list folders from
    task_folder = task_service.GetFolder(location)
    task_definition = task_folder.GetTask(name).Definition
    triggers = task_definition.Triggers

    ret = []
    for trigger in triggers:
        ret.append(trigger.Id)

    return ret


def list_actions(name, location='\\'):
    r'''
    List all actions that pertain to a task in the specified location.

    :param str name: The name of the task for which list actions.

    :param str location: A string value representing the location of the task
        from which to list actions. Default is '\\' which is the root for the
        task scheduler (C:\Windows\System32\tasks).

    :return: Returns a list of actions.
    :rtype: list

    CLI Example:

    .. code-block:: bash

        salt 'minion-id' task.list_actions <task_name>
    '''
    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the folder to list folders from
    task_folder = task_service.GetFolder(location)
    task_definition = task_folder.GetTask(name).Definition
    actions = task_definition.Actions

    ret = []
    for action in actions:
        ret.append(action.Id)

    return ret


def create_task(name,
                location='\\',
                user_name='System',
                password=None,
                force=False,
                **kwargs):
    r'''
    Create a new task in the designated location. This function has many keyword
    arguments that are not listed here. For additional arguments see:

    - :py:func:`edit_task`
                  description=None,
                  enabled=None,
                  hidden=None,
                  # Conditions Tab
                  run_if_idle=None,
                  idle_duration=None,
                  idle_wait_timeout=None,
                  idle_stop_on_end=None,
                  idle_restart=None,
                  ac_only=None,
                  stop_if_on_batteries=None,
                  wake_to_run=None,
                  run_if_network=None,
                  network_id=None,
                  network_name=None,
                  # Settings Tab
                  allow_demand_start=None,
                  start_when_available=None,
                  restart_every=None,
                  restart_count=3,
                  execution_time_limit=None,
                  force_stop=None,
                  delete_after=None,
                  multiple_instances=None,

    - :py:func:`add_action`
                action_type='Execute'

    - :py:func:`add_trigger`
                trigger_type=None,
                trigger_enabled=True,
                start_date=None,
                start_time=None,
                end_date=None,
                end_time=None,
                random_delay=None,
                repeat_interval=None,
                repeat_duration=None,
                repeat_stop_at_duration_end=False,
                execution_time_limit=None,
                delay=None

    :param str name: The name of the task. This will be displayed in the task
        scheduler.
    :param str location: A string value representing the location in which to
        create the task. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).
    :param str user_name: The user account under which to run the task. To
        specify the 'System' account, use 'System'. The password will be
        ignored.
    :param str password: The password to use for authentication. This should set
        the task to run whether the user is logged in or not, but is currently
        not working.
    :param bool force: If the task exists, overwrite the existing task.
    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    logging.debug("Creating the new task {0} in the designated location {1}...".format(repr(name), repr(location)))
    # Check for existing task
    if name in list_tasks(location) and not force:
        # Connect to an existing task definition
        logging.debug("The task {0} already exist in {1}. Nothing to do!".format(repr(name), repr(location)))
        return '{0} already exists'.format(name)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Create a new task definition
    task_definition = task_service.NewTask(0)

    #Get logon_type
    logon_type = kwargs.get('logon_type', None)
    if logon_type == None:
        logon_type = TASK_LOGON_INTERACTIVE_TOKEN

    # Modify task settings
    edit_task(task_definition=task_definition,
              user_name=user_name,
              password=password,
              **kwargs)

    # Add Action
    add_action(task_definition=task_definition, **kwargs)

    # Add Trigger
    add_trigger(task_definition=task_definition, **kwargs)

    # get the folder to create the task in
    task_folder = task_service.GetFolder(location)

    # Save the task
    _save_task_definition(name=name,
                          task_folder=task_folder,
                          task_definition=task_definition,
                          user_name=user_name,#task_definition.Principal.UserID,
                          password=password,
                          logon_type=logon_type)

    # Verify task was created
    if name in list_tasks(location):
        return True
    else:
        return False


def create_task_from_xml(name,
                         location='\\',
                         xml_text=None,
                         xml_path=None,
                         user_name='System',
                         password=None):
    r'''
    Create a task based on XML. Source can be a file or a string of XML.

    :param str name: The name of the task. This will be displayed in the task
        scheduler.

    :param str location: A string value representing the location in which to
        create the task. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).

    :param str xml_text: A string of xml representing the task to be created.
        This will be overridden by `xml_path` if passed.

    :param str xml_path: The path to an XML file on the local system containing
        the xml that defines the task. This will override `xml_text`

    :param str user_name: The user account under which to run the task. To
        specify the 'System' account, use 'System'. The password will be
        ignored.

    :param str password: The password to use for authentication. This should set
        the task to run whether the user is logged in or not, but is currently
        not working.

    :return: True if successful, False if unsuccessful
    :rtype: bool

    CLI Example:

    .. code-block:: bash

        salt 'minion-id' task.create_task_from_xml <task_name> xml_path=C:\task.xml
    '''
    # Check for existing task
    if name in list_tasks(location):
        # Connect to an existing task definition
        return '{0} already exists'.format(name)

    if not xml_text and not xml_path:
        return 'Must specify either xml_text or xml_path'

    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Load xml from file, overrides xml_text
    # Need to figure out how to load contents of xml
    if xml_path:
        xml_text = xml_path

    # Get the folder to list folders from
    task_folder = task_service.GetFolder(location)

    # Determine logon type
    if user_name:
        if user_name.lower() == 'system':
            logon_type = TASK_LOGON_SERVICE_ACCOUNT
            user_name = 'SYSTEM'
            password = None
        else:
            if password:
                logon_type = TASK_LOGON_PASSWORD
            else:
                logon_type = TASK_LOGON_INTERACTIVE_TOKEN
    else:
        password = None

    # Save the task
    try:
        task_folder.RegisterTask(name,
                                 xml_text,
                                 TASK_CREATE,
                                 user_name,
                                 password,
                                 logon_type)

    except pythoncom.com_error as error:
        hr, msg, exc, arg = error.args  # pylint: disable=W0633
        fc = {-2147216615: 'Required element or attribute missing',
              -2147216616: 'Value incorrectly formatted or out of range',
              -2147352571: 'Access denied'}
        try:
            failure_code = fc[exc[5]]
        except KeyError:
            failure_code = 'Unknown Failure: {0}'.format(error)

        logging.debug('Failed to create task: %s', failure_code)

    # Verify creation
    if name in list_tasks(location):
        return True
    else:
        return False


def create_folder(name, location='\\'):
    r'''
    Create a folder in which to create tasks.

    :param str name: The name of the folder. This will be displayed in the task
        scheduler.

    :param str location: A string value representing the location in which to
        create the folder. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing folder
    if name in list_folders(location):
        # Connect to an existing task definition
        return '{0} already exists'.format(name)

    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the folder to list folders from
    task_folder = task_service.GetFolder(location)
    task_folder.CreateFolder(name)

    # Verify creation
    if name in list_folders(location):
        return True
    else:
        return False


def edit_task(name=None,
              location='\\',
              # General Tab
              user_name=None,
              password=None,
              description=None,
              enabled=None,
              hidden=None,
              # Conditions Tab
              run_if_idle=None,
              idle_duration=None,
              idle_wait_timeout=None,
              idle_stop_on_end=None,
              idle_restart=None,
              ac_only=None,
              stop_if_on_batteries=None,
              wake_to_run=None,
              run_if_network=None,
              network_id=None,
              network_name=None,
              # Settings Tab
              allow_demand_start=None,
              start_when_available=None,
              restart_every=None,
              restart_count=3,
              execution_time_limit=None,
              force_stop=None,
              delete_after=None,
              multiple_instances=None,
              restorePrivs=False,
              privsToRestore=None,
              **kwargs):
    r'''
    Edit the parameters of a task. Triggers and Actions cannot be edited yet.

    :param str name: The name of the task. This will be displayed in the task
        scheduler.

    :param str location: A string value representing the location in which to
        create the task. Default is '\\' which is the root for the task
        scheduler (C:\Windows\System32\tasks).

    :param str user_name: The user account under which to run the task. To
        specify the 'System' account, use 'System'. The password will be
        ignored.

    :param str password: The password to use for authentication. This should set
        the task to run whether the user is logged in or not, but is currently
        not working.

    .. note::
        The combination of user_name and password determine how the task runs.
        For example, if a username is passed without at password the task will
        only run when the user is logged in. If a password is passed as well
        the task will run whether the user is logged on or not. If you pass
        'System' as the username the task will run as the system account (the
        password parameter is ignored.

    :param str description: A string representing the text that will be
        displayed in the description field in the task scheduler.

    :param bool enabled: A boolean value representing whether or not the task is
        enabled.

    :param bool hidden: A boolean value representing whether or not the task is
        hidden.

    :param bool run_if_idle: Boolean value that indicates that the Task
        Scheduler will run the task only if the computer is in an idle state.

    :param str idle_duration: A value that indicates the amount of time that the
        computer must be in an idle state before the task is run. Valid values
        are:

    - 1 minute
    - 5 minutes
    - 10 minutes
    - 15 minutes
    - 30 minutes
    - 1 hour

    :param str idle_wait_timeout: A value that indicates the amount of time that
        the Task Scheduler will wait for an idle condition to occur. Valid
        values are:

    - Do not wait
    - 1 minute
    - 5 minutes
    - 10 minutes
    - 15 minutes
    - 30 minutes
    - 1 hour
    - 2 hours

    :param bool idle_stop_on_end: Boolean value that indicates that the Task
        Scheduler will terminate the task if the idle condition ends before the
        task is completed.

    :param bool idle_restart: Boolean value that indicates whether the task is
        restarted when the computer cycles into an idle condition more than
        once.

    :param bool ac_only: Boolean value that indicates that the Task Scheduler
        will launch the task only while on AC power.

    :param bool stop_if_on_batteries: Boolean value that indicates that the task
        will be stopped if the computer begins to run on battery power.

    :param bool wake_to_run: Boolean value that indicates that the Task
        Scheduler will wake the computer when it is time to run the task.

    :param bool run_if_network: Boolean value that indicates that the Task
        Scheduler will run the task only when a network is available.

    :param guid network_id: GUID value that identifies a network profile.

    :param str network_name: Sets the name of a network profile. The name is
        used for display purposes.

    :param bool allow_demand_start: Boolean value that indicates that the task
        can be started by using either the Run command or the Context menu.

    :param bool start_when_available: Boolean value that indicates that the Task
        Scheduler can start the task at any time after its scheduled time has
        passed.

    :param restart_every: A value that specifies the interval between task
        restart attempts. Valid values are:

    - False (to disable)
    - 1 minute
    - 5 minutes
    - 10 minutes
    - 15 minutes
    - 30 minutes
    - 1 hour
    - 2 hours

    :param int restart_count: The number of times the Task Scheduler will
        attempt to restart the task. Valid values are integers 1 - 999.

    :param execution_time_limit: The amount of time allowed to complete the
        task. Valid values are:

    - False (to disable)
    - 1 hour
    - 2 hours
    - 4 hours
    - 8 hours
    - 12 hours
    - 1 day
    - 3 days

    :param bool force_stop: Boolean value that indicates that the task may be
        terminated by using TerminateProcess.

    :param delete_after: The amount of time that the Task Scheduler will
        wait before deleting the task after it expires. Requires a trigger with
        an expiration date. Valid values are:

    - False (to disable)
    - Immediately
    - 30 days
    - 90 days
    - 180 days
    - 365 days

    :param str multiple_instances: Sets the policy that defines how the Task
        Scheduler deals with multiple instances of the task. Valid values are:

    - Parallel
    - Queue
    - No New Instance
    - Stop Existing

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # TODO: Add more detailed return for items changed
    logging.debug("Editing parameters of the task...")
    # Check for passed task_definition
    # If not passed, open a task definition for an existing task
    save_definition = False
    if kwargs.get('task_definition', False):
        task_definition = kwargs.get('task_definition')
    else:
        save_definition = True

        # Make sure a name was passed
        if not name:
            return 'Required parameter "name" not passed'

        # Make sure task exists to modify
        if name in list_tasks(location):

            # Connect to the task scheduler
            pythoncom.CoInitialize()
            task_service = win32com.client.Dispatch("Schedule.Service")
            task_service.Connect()

            # get the folder to create the task in
            task_folder = task_service.GetFolder(location)

            # Connect to an existing task definition
            task_definition = task_folder.GetTask(name).Definition

        else:
            # Not found and create_new not set, return not found
            return '{0} not found'.format(name)

    # General Information
    if save_definition:
        task_definition.RegistrationInfo.Author = ''
        task_definition.RegistrationInfo.Source = ''

    if description is not None:
        task_definition.RegistrationInfo.Description = description

    # General Information: Security Options
    if user_name:
        # Determine logon type
        if user_name.lower() == 'system':
            logon_type = TASK_LOGON_SERVICE_ACCOUNT
            user_name = 'SYSTEM'
            password = None
        else:
            task_definition.Principal.Id = user_name
            #the security logon method that is required to run the tasks that are associated with the principal
            #https://docs.microsoft.com/is-is/windows/win32/taskschd/principal-logontype
            if password:
                #Use a password for logging on the user. The password must be supplied at registration time
                logon_type = TASK_LOGON_PASSWORD
            else:
                #User must already be logged on. The task will be run only in an existing interactive session.
                logon_type = TASK_LOGON_INTERACTIVE_TOKEN

        #help(task_definition)
        #print("------------",task_definition.Principal.getDisplayName())
        task_definition.Principal.UserID = user_name
        task_definition.Principal.DisplayName = user_name
        task_definition.Principal.LogonType = logon_type
        task_definition.Principal.RunLevel = TASK_RUNLEVEL_HIGHEST
    else:
        user_name = None
        password = None

    #Restore privileges if required
    if restorePrivs==True:
        IID_IPrincipal2 = pythoncom.MakeIID("{248919AE-E345-4A6D-8AEB-E0D3165C904E}")
        iprincipal2 = task_definition.Principal._oleobj_.QueryInterface(IID_IPrincipal2, pythoncom.IID_IDispatch)
        principal2 = win32com.client.Dispatch(iprincipal2)
        if privsToRestore == None:
            logging.error("Internal error: Impossible to restore privs because 'restorePrivs' is not given while 'restorePrivs' is enabled")
        if restorePrivs == True:
            logging.debug("Restoring these privileges: {0}".format(privsToRestore))
            for aPrivName in privsToRestore:
                principal2.AddRequiredPrivilege(aPrivName)
        #print("-----",principal2.getRequiredPrivilege(0))

    # Settings
    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383480(v=vs.85).aspx
    if enabled is not None:
        task_definition.Settings.Enabled = enabled
    # Settings: General Tab
    if hidden is not None:
        task_definition.Settings.Hidden = hidden

    # Settings: Conditions Tab (Idle)
    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa380669(v=vs.85).aspx
    if run_if_idle is not None:
        task_definition.Settings.RunOnlyIfIdle = run_if_idle

    if task_definition.Settings.RunOnlyIfIdle:
        if idle_stop_on_end is not None:
            task_definition.Settings.IdleSettings.StopOnIdleEnd = idle_stop_on_end
        if idle_restart is not None:
            task_definition.Settings.IdleSettings.RestartOnIdle = idle_restart
        if idle_duration is not None:
            if idle_duration in duration:
                task_definition.Settings.IdleSettings.IdleDuration = _lookup_first(duration, idle_duration)
            else:
                return 'Invalid value for "idle_duration"'
        if idle_wait_timeout is not None:
            if idle_wait_timeout in duration:
                task_definition.Settings.IdleSettings.WaitTimeout = _lookup_first(duration, idle_wait_timeout)
            else:
                return 'Invalid value for "idle_wait_timeout"'

    # Settings: Conditions Tab (Power)
    if ac_only is not None:
        task_definition.Settings.DisallowStartIfOnBatteries = ac_only
    if stop_if_on_batteries is not None:
        task_definition.Settings.StopIfGoingOnBatteries = stop_if_on_batteries
    if wake_to_run is not None:
        task_definition.Settings.WakeToRun = wake_to_run

    # Settings: Conditions Tab (Network)
    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382067(v=vs.85).aspx
    if run_if_network is not None:
        task_definition.Settings.RunOnlyIfNetworkAvailable = run_if_network
    if task_definition.Settings.RunOnlyIfNetworkAvailable:
        if network_id:
            task_definition.Settings.NetworkSettings.Id = network_id
        if network_name:
            task_definition.Settings.NetworkSettings.Name = network_name

    # Settings: Settings Tab
    if allow_demand_start is not None:
        task_definition.Settings.AllowDemandStart = allow_demand_start
    if start_when_available is not None:
        task_definition.Settings.StartWhenAvailable = start_when_available
    if restart_every is not None:
        if restart_every is False:
            task_definition.Settings.RestartInterval = ''
        else:
            if restart_every in duration:
                task_definition.Settings.RestartInterval = _lookup_first(duration, restart_every)
            else:
                return 'Invalid value for "restart_every"'
    if task_definition.Settings.RestartInterval:
        if restart_count is not None:
            if restart_count in range(1, 999):
                task_definition.Settings.RestartCount = restart_count
            else:
                return '"restart_count" must be a value between 1 and 999'
    if execution_time_limit is not None:
        if execution_time_limit is False:
            task_definition.Settings.ExecutionTimeLimit = 'PT0S'
        else:
            if execution_time_limit in duration:
                task_definition.Settings.ExecutionTimeLimit = _lookup_first(duration, execution_time_limit)
            else:
                return 'Invalid value for "execution_time_limit"'
    if force_stop is not None:
        task_definition.Settings.AllowHardTerminate = force_stop
    if delete_after is not None:
        # TODO: Check triggers for end_boundary
        if delete_after is False:
            task_definition.Settings.DeleteExpiredTaskAfter = ''
        if delete_after in duration:
            task_definition.Settings.DeleteExpiredTaskAfter = _lookup_first(duration, delete_after)
        else:
            return 'Invalid value for "delete_after"'
    if multiple_instances is not None:
        task_definition.Settings.MultipleInstances = instances[multiple_instances]

    # Save the task
    if save_definition:
        # Save the Changes
        return _save_task_definition(name=name,
                                     task_folder=task_folder,
                                     task_definition=task_definition,
                                     user_name=user_name,
                                     password=password,
                                     logon_type=task_definition.Principal.LogonType)


def delete_task(name, location='\\'):
    r'''
    Delete a task from the task scheduler.

    :param str name: The name of the task to delete.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing task
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the task from
    task_folder = task_service.GetFolder(location)

    task_folder.DeleteTask(name, 0)

    # Verify deletion
    if name not in list_tasks(location):
        logging.debug("The task {0} has been removed".format(repr(name)))
        return True
    else:
        logging.error("The task {0} has NOT been removed".format(repr(name)))
        return False


def delete_folder(name, location='\\'):
    r'''
    Delete a folder from the task scheduler.

    :param str name: The name of the folder to delete.

    :param str location: A string value representing the location of the
        folder.  Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing folder
    if name not in list_folders(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the folder from
    task_folder = task_service.GetFolder(location)

    # Delete the folder
    task_folder.DeleteFolder(name, 0)

    # Verify deletion
    if name not in list_folders(location):
        return True
    else:
        return False


def run(name, location='\\'):
    r'''
    Run a scheduled task manually.

    :param str name: The name of the task to run.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing folder
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the folder from
    task_folder = task_service.GetFolder(location)
    task = task_folder.GetTask(name)

    try:
        task.Run('')
        logging.debug("Task {0} executed successfully".format(repr(name)))
        return True
    except pythoncom.com_error as e:
        logging.error("Impossible to execute task {0}".format(repr(name, e)))
        return False


def run_wait(name, location='\\'):
    r'''
    Run a scheduled task and return when the task finishes

    :param str name: The name of the task to run.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing folder
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the folder from
    task_folder = task_service.GetFolder(location)
    task = task_folder.GetTask(name)

    # Is the task already running
    if task.State == TASK_STATE_RUNNING:
        return 'Task already running'

    try:
        task.Run('')
        time.sleep(1)
        running = True
    except pythoncom.com_error:
        return False

    while running:
        running = False
        try:
            running_tasks = task_service.GetRunningTasks(0)
            if running_tasks.Count:
                for item in running_tasks:
                    if item.Name == name:
                        running = True
        except pythoncom.com_error:
            running = False

    return True


def stop(name, location='\\'):
    r'''
    Stop a scheduled task.

    :param str name: The name of the task to stop.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing folder
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the folder from
    task_folder = task_service.GetFolder(location)
    task = task_folder.GetTask(name)

    try:
        task.Stop(0)
        return True
    except pythoncom.com_error as error:
        return False


def status(name, location='\\'):
    r'''
    Determine the status of a task. Is it Running, Queued, Ready, etc.

    :param str name: The name of the task for which to return the status

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: The current status of the task. Will be one of the following:

    - Unknown
    - Disabled
    - Queued
    - Ready
    - Running

    :rtype: string
    '''
    # Check for existing folder
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder where the task is defined
    task_folder = task_service.GetFolder(location)
    task = task_folder.GetTask(name)

    return states[task.State]


def info(name, location='\\'):
    r'''
    Get the details about a task in the task scheduler.

    :param str name: The name of the task for which to return the status

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return:
    :rtype: dict
    '''
    # Check for existing folder
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # connect to the task scheduler
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # get the folder to delete the folder from
    task_folder = task_service.GetFolder(location)
    task = task_folder.GetTask(name)

    properties = {'enabled': task.Enabled,
                  'last_run': _get_date_value(task.LastRunTime),
                  'last_run_result': results[task.LastTaskResult],
                  'missed_runs': task.NumberOfMissedRuns,
                  'next_run': _get_date_value(task.NextRunTime),
                  'status': states[task.State]}

    def_set = task.Definition.Settings

    settings = {}
    settings['allow_demand_start'] = def_set.AllowDemandStart
    settings['force_stop'] = def_set.AllowHardTerminate

    if def_set.DeleteExpiredTaskAfter == '':
        settings['delete_after'] = False
    elif def_set.DeleteExpiredTaskAfter == 'PT0S':
        settings['delete_after'] = 'Immediately'
    else:
        settings['delete_after'] = _reverse_lookup(duration, def_set.DeleteExpiredTaskAfter)

    if def_set.ExecutionTimeLimit == '':
        settings['execution_time_limit'] = False
    else:
        settings['execution_time_limit'] = _reverse_lookup(duration, def_set.ExecutionTimeLimit)

    settings['multiple_instances'] = _reverse_lookup(instances, def_set.MultipleInstances)

    if def_set.RestartInterval == '':
        settings['restart_interval'] = False
    else:
        settings['restart_interval'] = _reverse_lookup(duration, def_set.RestartInterval)

    if settings['restart_interval']:
        settings['restart_count'] = def_set.RestartCount
    settings['stop_if_on_batteries'] = def_set.StopIfGoingOnBatteries
    settings['wake_to_run'] = def_set.WakeToRun

    conditions = {}
    conditions['ac_only'] = def_set.DisallowStartIfOnBatteries
    conditions['run_if_idle'] = def_set.RunOnlyIfIdle
    conditions['run_if_network'] = def_set.RunOnlyIfNetworkAvailable
    conditions['start_when_available'] = def_set.StartWhenAvailable

    if conditions['run_if_idle']:
        idle_set = def_set.IdleSettings
        conditions['idle_duration'] = idle_set.IdleDuration
        conditions['idle_restart'] = idle_set.RestartOnIdle
        conditions['idle_stop_on_end'] = idle_set.StopOnIdleEnd
        conditions['idle_wait_timeout'] = idle_set.WaitTimeout

    if conditions['run_if_network']:
        net_set = def_set.NetworkSettings
        conditions['network_id'] = net_set.Id
        conditions['network_name'] = net_set.Name

    actions = []
    for actionObj in task.Definition.Actions:
        action = {}
        action['action_type'] = _reverse_lookup(action_types, actionObj.Type)
        if actionObj.Path:
            action['cmd'] = actionObj.Path
        if actionObj.Arguments:
            action['arguments'] = actionObj.Arguments
        if actionObj.WorkingDirectory:
            action['working_dir'] = actionObj.WorkingDirectory
        actions.append(action)

    triggers = []
    for triggerObj in task.Definition.Triggers:
        trigger = {}
        trigger['trigger_type'] = _reverse_lookup(trigger_types, triggerObj.Type)
        if triggerObj.ExecutionTimeLimit:
            trigger['execution_time_limit'] = _reverse_lookup(duration, triggerObj.ExecutionTimeLimit)
        if triggerObj.StartBoundary:
            start_date, start_time = triggerObj.StartBoundary.split('T', 1)
            trigger['start_date'] = start_date
            trigger['start_time'] = start_time
        if triggerObj.EndBoundary:
            end_date, end_time = triggerObj.EndBoundary.split('T', 1)
            trigger['end_date'] = end_date
            trigger['end_time'] = end_time
        trigger['enabled'] = triggerObj.Enabled
        if hasattr(triggerObj, 'RandomDelay'):
            if triggerObj.RandomDelay:
                trigger['random_delay'] = _reverse_lookup(duration, triggerObj.RandomDelay)
            else:
                trigger['random_delay'] = False
        if hasattr(triggerObj, 'Delay'):
            if triggerObj.Delay:
                trigger['delay'] = _reverse_lookup(duration, triggerObj.Delay)
            else:
                trigger['delay'] = False
        triggers.append(trigger)

    properties['settings'] = settings
    properties['conditions'] = conditions
    properties['actions'] = actions
    properties['triggers'] = triggers
    ret = properties

    return ret


def add_action(name=None,
               location='\\',
               action_type='Execute',
               **kwargs):
    r'''
    Add an action to a task.

    :param str name: The name of the task to which to add the action.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :param str action_type: The type of action to add. There are three action
        types. Each one requires its own set of Keyword Arguments (kwargs). Valid
        values are:

        - Execute
        - Email
        - Message

    Required arguments for each action_type:

    **Execute** - Execute a command or an executable

    :param str cmd: (required) The command / executable to run.

    :param str arguments: (optional) Arguments to be passed to the command /
        executable. To launch a script the first command will need to be the
        interpreter for the script. For example, to run a vbscript you would
        pass `cscript.exe` in the `cmd` parameter and pass the script in the
        `arguments` parameter as follows:

        - ``cmd='cscript.exe' arguments='c:\scripts\myscript.vbs'``

        Batch files do not need an interpreter and may be passed to the cmd
        parameter directly.

    :param str start_in: (optional) The current working directory for the
        command.

    **Email** - Send and email. Requires ``server``, ``from``, and ``to`` or
    ``cc``.

    :param str from: The sender
    :param str reply_to: Who to reply to
    :param str to: The recipient
    :param str cc: The CC recipient
    :param str bcc: The BCC recipient
    :param str subject: The subject of the email
    :param str body: The Message Body of the email
    :param str server: The server used to send the email
    :param list attachments: A list of attachments. These will be the paths to
        the files to attach. ie: ``attachments="['C:\attachment1.txt',
        'C:\attachment2.txt']"``

    **Message** - Display a dialog box. The task must be set to "Run only when
    user is logged on" in order for the dialog box to display. Both parameters
    are required.

    :param str title: The dialog box title.
    :param str message: The dialog box message body

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    logging.debug("Adding an action to the task...")
    save_definition = False
    if kwargs.get('task_definition', False):
        task_definition = kwargs.get('task_definition')
    else:
        save_definition = True
        # Make sure a name was passed
        if not name:
            return 'Required parameter "name" not passed'

        # Make sure task exists
        if name in list_tasks(location):

            # Connect to the task scheduler
            pythoncom.CoInitialize()
            task_service = win32com.client.Dispatch("Schedule.Service")
            task_service.Connect()

            # get the folder to create the task in
            task_folder = task_service.GetFolder(location)

            # Connect to an existing task definition
            task_definition = task_folder.GetTask(name).Definition

        else:
            # Not found and create_new not set, return not found
            return '{0} not found'.format(name)

    # Action Settings
    task_action = task_definition.Actions.Create(action_types[action_type])
    if action_types[action_type] == TASK_ACTION_EXEC:
        task_action.Id = 'Execute_ID1'
        if kwargs.get('cmd', False):
            task_action.Path = kwargs.get('cmd')
        else:
            return 'Required parameter "cmd" not found'
        task_action.Arguments = kwargs.get('arguments', '')
        task_action.WorkingDirectory = kwargs.get('start_in', '')

    elif action_types[action_type] == TASK_ACTION_SEND_EMAIL:
        task_action.Id = 'Email_ID1'

        # Required Parameters
        if kwargs.get('server', False):
            task_action.Server = kwargs.get('server')
        else:
            return 'Required parameter "server" not found'

        if kwargs.get('from', False):
            task_action.From = kwargs.get('from')
        else:
            return 'Required parameter "from" not found'

        if kwargs.get('to', False) or kwargs.get('cc', False):
            if kwargs.get('to'):
                task_action.To = kwargs.get('to')
            if kwargs.get('cc'):
                task_action.Cc = kwargs.get('cc')
        else:
            return 'Required parameter "to" or "cc" not found'

        # Optional Parameters
        if kwargs.get('reply_to'):
            task_action.ReplyTo = kwargs.get('reply_to')
        if kwargs.get('bcc'):
            task_action.Bcc = kwargs.get('bcc')
        if kwargs.get('subject'):
            task_action.Subject = kwargs.get('subject')
        if kwargs.get('body'):
            task_action.Body = kwargs.get('body')
        if kwargs.get('attachments'):
            task_action.Attachments = kwargs.get('attachments')

    elif action_types[action_type] == TASK_ACTION_SHOW_MESSAGE:
        task_action.Id = 'Message_ID1'

        if kwargs.get('title', False):
            task_action.Title = kwargs.get('title')
        else:
            return 'Required parameter "title" not found'

        if kwargs.get('message', False):
            task_action.MessageBody = kwargs.get('message')
        else:
            return 'Required parameter "message" not found'

    # Save the task
    if save_definition:
        # Save the Changes
        return _save_task_definition(name=name,
                                     task_folder=task_folder,
                                     task_definition=task_definition,
                                     user_name=task_definition.Principal.UserID,
                                     password=None,
                                     logon_type=task_definition.Principal.LogonType)


def _clear_actions(name, location='\\'):
    r'''
    Remove all actions from the task.

    :param str name: The name of the task from which to clear all actions.

    :param str location: A string value representing the location of the task.
    Default is '\\' which is the root for the task scheduler
    (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # TODO: The problem is, you have to have at least one action for the task to
    # TODO: be valid, so this will always fail with a 'Required element or
    # TODO: attribute missing' error.
    # TODO: Make this an internal function that clears the functions but doesn't
    # TODO: save it. Then you can add a new function. Maybe for editing an
    # TODO: action.
    # Check for existing task
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the actions from the task
    task_folder = task_service.GetFolder(location)
    task_definition = task_folder.GetTask(name).Definition
    actions = task_definition.Actions

    actions.Clear()

    # Save the Changes
    return _save_task_definition(name=name,
                                 task_folder=task_folder,
                                 task_definition=task_definition,
                                 user_name=task_definition.Principal.UserID,
                                 password=None,
                                 logon_type=task_definition.Principal.LogonType)


def add_trigger(name=None,
                location='\\',
                trigger_type=None,
                trigger_enabled=True,
                start_date=None,
                start_time=None,
                end_date=None,
                end_time=None,
                random_delay=None,
                repeat_interval=None,
                repeat_duration=None,
                repeat_stop_at_duration_end=False,
                execution_time_limit=None,
                delay=None,
                **kwargs):
    r'''
    :param str name: The name of the task to which to add the trigger.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :param str trigger_type: The type of trigger to create. This is defined
        when the trigger is created and cannot be changed later. Options are as
        follows:

    - Event
    - Once
    - Daily
    - Weekly
    - Monthly
    - MonthlyDay
    - OnIdle
    - OnTaskCreation
    - OnBoot
    - OnLogon
    - OnSessionChange

    :param bool trigger_enabled: Boolean value that indicates whether the
        trigger is enabled.

    :param str start_date: The date when the trigger is activated. If no value
        is passed, the current date will be used. Can be one of the following
        formats:

    - %Y-%m-%d
    - %m-%d-%y
    - %m-%d-%Y
    - %m/%d/%y
    - %m/%d/%Y
    - %Y/%m/%d

    :param str start_time: The time when the trigger is activated. If no value
        is passed, midnight will be used. Can be one of the following formats:

    - %I:%M:%S %p
    - %I:%M %p
    - %H:%M:%S
    - %H:%M

    :param str end_date: The date when the trigger is deactivated. The trigger
        cannot start the task after it is deactivated. Can be one of the
        following formats:

    - %Y-%m-%d
    - %m-%d-%y
    - %m-%d-%Y
    - %m/%d/%y
    - %m/%d/%Y
    - %Y/%m/%d

    :param str end_time: The time when the trigger is deactivated. If the this
        is not passed with ``end_date`` it will be set to midnight. Can be one
        of the following formats:

    - %I:%M:%S %p
    - %I:%M %p
    - %H:%M:%S
    - %H:%M

    :param str random_delay: The delay time that is randomly added to the start
        time of the trigger. Valid values are:

    - 30 seconds
    - 1 minute
    - 30 minutes
    - 1 hour
    - 8 hours
    - 1 day

    :param str repeat_interval: The amount of time between each restart of the
        task. Valid values are:

    - 5 minutes
    - 10 minutes
    - 15 minutes
    - 30 minutes
    - 1 hour

    :param str repeat_duration: How long the pattern is repeated. Valid values
        are:

    - Indefinitely
    - 15 minutes
    - 30 minutes
    - 1 hour
    - 12 hours
    - 1 day

    :param bool repeat_stop_at_duration_end: Boolean value that indicates if a
        running instance of the task is stopped at the end of the repetition
        pattern duration.

    :param str execution_time_limit: The maximum amount of time that the task
        launched by the trigger is allowed to run. Valid values are:

    - 30 minutes
    - 1 hour
    - 2 hours
    - 4 hours
    - 8 hours
    - 12 hours
    - 1 day
    - 3 days (default)

    :param str delay: The time the trigger waits after its activation to start the task.
        Valid values are:

        - 15 seconds
        - 30 seconds
        - 1 minute
        - 30 minutes
        - 1 hour
        - 8 hours
        - 1 day

    **kwargs**

    There are optional keyword arguments determined by the type of trigger
    being defined. They are as follows:

    *Event*

    :param str subscription: An event definition in xml format that fires the
        trigger. The easiest way to get this would is to create an event in
        windows task scheduler and then copy the xml text.

    *Once*

    No special parameters required.

    *Daily*

    :param int days_interval: The interval between days in the schedule. An
        interval of 1 produces a daily schedule. An interval of 2 produces an
        every-other day schedule. If no interval is specified, 1 is used. Valid
        entries are 1 - 999.

    *Weekly*

    :param int weeks_interval: The interval between weeks in the schedule.
        An interval of 1 produces a weekly schedule. An interval of 2 produces
        an every-other week schedule. If no interval is specified, 1 is used.
        Valid entries are 1 - 52.

    param list days_of_week: Sets the days of the week on which the task
        runs. Should be a list. ie: ['Monday','Wednesday','Friday']. Valid
        entries are the names of the days of the week.

    *Monthly*

    :param list months_of_year: Sets the months of the year during which the
        task runs. Should be a list. ie: ['January','July']. Valid entries are
        the full names of all the months.

    :param list days_of_month: Sets the days of the month during which the
        task runs. Should be a list. ie: [1, 15, 'Last']. Options are all days
        of the month 1 - 31 and the word 'Last' to indicate the last day of the
        month.

    :param bool last_day_of_month: Boolean value that indicates that the
        task runs on the last day of the month regardless of the actual date of
        that day.

    You can set the task to run on the last day of the month by either
    including the word 'Last' in the list of days, or setting the parameter
    'last_day_of_month` equal to True.

    *MonthlyDay*

    :param list months_of_year: Sets the months of the year during which the
        task runs. Should be a list. ie: ['January','July']. Valid entries are
        the full names of all the months.

    :param list weeks_of_month: Sets the weeks of the month during which the
        task runs. Should be a list. ie: ['First','Third']. Valid options are:

    - First
    - Second
    - Third
    - Fourth

    :param bool last_week_of_month: Boolean value that indicates that the task
        runs on the last week of the month.

    :param list days_of_week: Sets the days of the week during which the task
        runs. Should be a list. ie: ['Monday','Wednesday','Friday'].  Valid
        entries are the names of the days of the week.

    *OnIdle*
    No special parameters required.

    *OnTaskCreation*
    No special parameters required.

    *OnBoot*
    No special parameters required.

    *OnLogon*
    No special parameters required.

    *OnSessionChange*

    :param str session_user_name: Sets the user for the Terminal Server
        session. When a session state change is detected for this user, a task
        is started. To detect session status change for any user, do not pass
        this parameter.

    :param str state_change: Sets the kind of Terminal Server session change
        that would trigger a task launch. Valid options are:

    - ConsoleConnect: When you connect to a user session (switch users)
    - ConsoleDisconnect: When you disconnect a user session (switch users)
    - RemoteConnect: When a user connects via Remote Desktop
    - RemoteDisconnect: When a user disconnects via Remote Desktop
    - SessionLock: When the workstation is locked
    - SessionUnlock: When the workstation is unlocked

    .. note::

        Arguments are parsed by the YAML loader and are subject to yaml's
        idiosyncrasies. Therefore, time values in some formats (``%H:%M:%S`` and
        ``%H:%M``) should to be quoted. See `YAML IDIOSYNCRASIES`_ for more details.

    .. _`YAML IDIOSYNCRASIES`: https://docs.saltstack.com/en/latest/topics/troubleshooting/yaml_idiosyncrasies.html#time-expressions


    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    logging.debug("Creating a Trigger...")
    if not trigger_type:
        return 'Required parameter "trigger_type" not specified'

    # Define lookup dictionaries
    state_changes = {'ConsoleConnect': 1,
                     'ConsoleDisconnect': 2,
                     'RemoteConnect': 3,
                     'RemoteDisconnect': 4,
                     'SessionLock': 7,
                     'SessionUnlock': 8}

    days = {1: 0x1,
            2: 0x2,
            3: 0x4,
            4: 0x8,
            5: 0x10,
            6: 0x20,
            7: 0x40,
            8: 0x80,
            9: 0x100,
            10: 0x200,
            11: 0x400,
            12: 0x800,
            13: 0x1000,
            14: 0x2000,
            15: 0x4000,
            16: 0x8000,
            17: 0x10000,
            18: 0x20000,
            19: 0x40000,
            20: 0x80000,
            21: 0x100000,
            22: 0x200000,
            23: 0x400000,
            24: 0x800000,
            25: 0x1000000,
            26: 0x2000000,
            27: 0x4000000,
            28: 0x8000000,
            29: 0x10000000,
            30: 0x20000000,
            31: 0x40000000,
            'Last': 0x80000000}

    weekdays = {'Sunday': 0x1,
                'Monday': 0x2,
                'Tuesday': 0x4,
                'Wednesday': 0x8,
                'Thursday': 0x10,
                'Friday': 0x20,
                'Saturday': 0x40}

    weeks = {'First': 0x1,
             'Second': 0x2,
             'Third': 0x4,
             'Fourth': 0x8}

    months = {'January': 0x1,
              'February': 0x2,
              'March': 0x4,
              'April': 0x8,
              'May': 0x10,
              'June': 0x20,
              'July': 0x40,
              'August': 0x80,
              'September': 0x100,
              'October': 0x200,
              'November': 0x400,
              'December': 0x800}

    # Format Date Parameters
    if start_date:
        date_format = _get_date_time_format(start_date)
        if date_format:
            dt_obj = datetime.strptime(start_date, date_format)
        else:
            return 'Invalid start_date'
    else:
        dt_obj = datetime.now()

    if start_time:
        time_format = _get_date_time_format(start_time)
        if time_format:
            tm_obj = datetime.strptime(start_time, time_format)
        else:
            return 'Invalid start_time'
    else:
        tm_obj = datetime.strptime('00:00:00', '%H:%M:%S')

    start_boundary = '{0}T{1}'.format(dt_obj.strftime('%Y-%m-%d'),
                                      tm_obj.strftime('%H:%M:%S'))

    dt_obj = None
    tm_obj = None
    if end_date:
        date_format = _get_date_time_format(end_date)
        if date_format:
            dt_obj = datetime.strptime(end_date, date_format)
        else:
            return 'Invalid end_date'

    if end_time:
        time_format = _get_date_time_format(end_time)
        if time_format:
            tm_obj = datetime.strptime(end_time, time_format)
        else:
            return 'Invalid end_time'
    else:
        tm_obj = datetime.strptime('00:00:00', '%H:%M:%S')

    end_boundary = None
    if dt_obj and tm_obj:
        end_boundary = '{0}T{1}'.format(dt_obj.strftime('%Y-%m-%d'),
                                        tm_obj.strftime('%H:%M:%S'))

    save_definition = False
    if kwargs.get('task_definition', False):
        task_definition = kwargs.get('task_definition')
    else:
        save_definition = True
        # Make sure a name was passed
        if not name:
            return 'Required parameter "name" not passed'

        # Make sure task exists
        if name in list_tasks(location):

            # Connect to the task scheduler
            pythoncom.CoInitialize()
            task_service = win32com.client.Dispatch("Schedule.Service")
            task_service.Connect()

            # get the folder to create the task in
            task_folder = task_service.GetFolder(location)

            # Connect to an existing task definition
            task_definition = task_folder.GetTask(name).Definition

        else:
            # Not found and create_new not set, return not found
            return '{0} not found'.format(name)

    # Create a New Trigger
    trigger = task_definition.Triggers.Create(trigger_types[trigger_type])

    # Shared Trigger Parameters
    # Settings
    trigger.StartBoundary = start_boundary
    # Advanced Settings
    if delay:
        trigger.Delay = _lookup_first(duration, delay)
    if random_delay:
        trigger.RandomDelay = _lookup_first(duration, random_delay)
    if repeat_interval:
        trigger.Repetition.Interval = _lookup_first(duration, repeat_interval)
        if repeat_duration:
            trigger.Repetition.Duration = _lookup_first(duration, repeat_duration)
        trigger.Repetition.StopAtDurationEnd = repeat_stop_at_duration_end
    if execution_time_limit:
        trigger.ExecutionTimeLimit = _lookup_first(duration, execution_time_limit)
    if end_boundary:
        trigger.EndBoundary = end_boundary
    trigger.Enabled = trigger_enabled

    # Trigger Specific Parameters
    # Event Trigger Parameters
    if trigger_types[trigger_type] == TASK_TRIGGER_EVENT:
        # Check for required kwargs
        if kwargs.get('subscription', False):
            trigger.Id = 'Event_ID1'
            trigger.Subscription = kwargs.get('subscription')
        else:
            return 'Required parameter "subscription" not passed'

    elif trigger_types[trigger_type] == TASK_TRIGGER_TIME:
        trigger.Id = 'Once_ID1'

    # Daily Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_DAILY:
        trigger.Id = 'Daily_ID1'
        trigger.DaysInterval = kwargs.get('days_interval', 1)

    # Weekly Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_WEEKLY:
        trigger.Id = 'Weekly_ID1'
        trigger.WeeksInterval = kwargs.get('weeks_interval', 1)
        if kwargs.get('days_of_week', False):
            bits_days = 0
            for weekday in kwargs.get('days_of_week'):
                bits_days |= weekdays[weekday]
            trigger.DaysOfWeek = bits_days
        else:
            return 'Required parameter "days_of_week" not passed'

    # Monthly Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_MONTHLY:
        trigger.Id = 'Monthly_ID1'
        if kwargs.get('months_of_year', False):
            bits_months = 0
            for month in kwargs.get('months_of_year'):
                bits_months |= months[month]
            trigger.MonthsOfYear = bits_months
        else:
            return 'Required parameter "months_of_year" not passed'

        if kwargs.get('days_of_month', False) or \
                kwargs.get('last_day_of_month', False):
            if kwargs.get('days_of_month', False):
                bits_days = 0
                for day in kwargs.get('days_of_month'):
                    bits_days |= days[day]
                trigger.DaysOfMonth = bits_days
            trigger.RunOnLastDayOfMonth = kwargs.get('last_day_of_month', False)
        else:
            return 'Monthly trigger requires "days_of_month" or "last_day_of_month" parameters'

    # Monthly Day Of Week Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_MONTHLYDOW:
        trigger.Id = 'Monthy_DOW_ID1'
        if kwargs.get('months_of_year', False):
            bits_months = 0
            for month in kwargs.get('months_of_year'):
                bits_months |= months[month]
            trigger.MonthsOfYear = bits_months
        else:
            return 'Required parameter "months_of_year" not passed'

        if kwargs.get('weeks_of_month', False) or \
                kwargs.get('last_week_of_month', False):
            if kwargs.get('weeks_of_month', False):
                bits_weeks = 0
                for week in kwargs.get('weeks_of_month'):
                    bits_weeks |= weeks[week]
                trigger.WeeksOfMonth = bits_weeks
            trigger.RunOnLastWeekOfMonth = kwargs.get('last_week_of_month', False)
        else:
            return 'Monthly DOW trigger requires "weeks_of_month" or "last_week_of_month" parameters'

        if kwargs.get('days_of_week', False):
            bits_days = 0
            for weekday in kwargs.get('days_of_week'):
                bits_days |= weekdays[weekday]
            trigger.DaysOfWeek = bits_days
        else:
            return 'Required parameter "days_of_week" not passed'

    # On Idle Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_IDLE:
        trigger.Id = 'OnIdle_ID1'

    # On Task Creation Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_REGISTRATION:
        trigger.Id = 'OnTaskCreation_ID1'

    # On Boot Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_BOOT:
        trigger.Id = 'OnBoot_ID1'

    # On Logon Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_LOGON:
        trigger.Id = 'OnLogon_ID1'

    # On Session State Change Trigger Parameters
    elif trigger_types[trigger_type] == TASK_TRIGGER_SESSION_STATE_CHANGE:
        trigger.Id = 'OnSessionStateChange_ID1'
        if kwargs.get('session_user_name', False):
            trigger.UserId = kwargs.get('session_user_name')
        if kwargs.get('state_change', False):
            trigger.StateChange = state_changes[kwargs.get('state_change')]
        else:
            return 'Required parameter "state_change" not passed'

    # Save the task
    if save_definition:
        # Save the Changes
        return _save_task_definition(name=name,
                                     task_folder=task_folder,
                                     task_definition=task_definition,
                                     user_name=task_definition.Principal.UserID,
                                     password=None,
                                     logon_type=task_definition.Principal.LogonType)


def clear_triggers(name, location='\\'):
    r'''
    Remove all triggers from the task.

    :param str name: The name of the task from which to clear all triggers.

    :param str location: A string value representing the location of the task.
        Default is '\\' which is the root for the task scheduler
        (C:\Windows\System32\tasks).

    :return: True if successful, False if unsuccessful
    :rtype: bool
    '''
    # Check for existing task
    if name not in list_tasks(location):
        return '{0} not found in {1}'.format(name, location)

    # Create the task service object
    pythoncom.CoInitialize()
    task_service = win32com.client.Dispatch("Schedule.Service")
    task_service.Connect()

    # Get the triggers from the task
    task_folder = task_service.GetFolder(location)
    task_definition = task_folder.GetTask(name).Definition
    triggers = task_definition.Triggers

    triggers.Clear()

    # Save the Changes
    return _save_task_definition(name=name,
                                 task_folder=task_folder,
                                 task_definition=task_definition,
                                 user_name=task_definition.Principal.UserID,
                                 password=None,
                                 logon_type=task_definition.Principal.LogonType)

def createScheduledTask(taskName,
                        username,
                        cmd,
                        args,
                        workingDir='',
                        location="\\",
                        hidden=True,
                        logonType=TASK_LOGON_INTERACTIVE_TOKEN,
                        flagsCreation=TASK_CREATE_OR_UPDATE):

    service = win32com.client.Dispatch("Schedule.Service")
    service.Connect()
    rootFolder = service.GetFolder(location)
    taskDefinition = service.NewTask(0)
    #Principal
    taskDefinition.Principal.LogonType = logonType
    taskDefinition.Principal.UserID = username
    #taskDefinition.Principal.Id = username
    #taskDefinition.Principal.DisplayName = username
    #taskDefinition.Principal.RunLevel = TASK_RUNLEVEL_HIGHEST

    IID_IPrincipal2 = pythoncom.MakeIID("{248919AE-E345-4A6D-8AEB-E0D3165C904E}")
    iprincipal2 = taskDefinition.Principal._oleobj_.QueryInterface(IID_IPrincipal2, pythoncom.IID_IDispatch)
    principal2 = win32com.client.Dispatch(iprincipal2)
    for aPrivName in NETWORK_SERVICE_REQUIRED_PRIVS_EXTENDED:
        principal2.AddRequiredPrivilege(aPrivName)

    '''
    taskDefinition.Settings.Enabled = True  # If True, the task is enabled.
    taskDefinition.Settings.StartWhenAvailable = True
    taskDefinition.Settings.Hidden = hidden  # If False, the task will be visible in the UI. The default is False.
    taskDefinition.Settings.MultipleInstances = 0  # Starts a new instance while an existing instance of the task is running.
    '''

    # Add an action to the task to run notepad.exe.
    Action = taskDefinition.Actions.Create(TASK_ACTION_EXEC)
    Action.Path = cmd
    Action.Arguments = args
    #Action.WorkingDirectory = workingDir

    logging.debug("Task definition created. Submiting the task for executing {0} {1}".format(repr(cmd), repr(args)))

    # ***********************************************************
    # Register (create) the task.
    #HRESULT RegisterTaskDefinition(
    # BSTR            path,
    # ITaskDefinition *pDefinition,
    # LONG            flags,
    # VARIANT         userId,
    # VARIANT         password,
    # TASK_LOGON_TYPE logonType,
    # VARIANT         sddl,
    # IRegisteredTask **ppTask
    # );
    try:
        rootFolder.RegisterTaskDefinition(taskName,
                                          taskDefinition,
                                          flagsCreation,
                                          None, #userId
                                          None, #password
                                          logonType)
        logging.debug("The task {0} is registered or updated successfully".format(repr(taskName)))
    except pythoncom.com_error as error:
        #https://knowledge.broadcom.com/external/article/152212/error-codes-list-for-microsoft-technolog.html
        hr, msg, exc, arg = error.args  # pylint: disable=W0633
        fc = {-2147024773: 'The filename, directory name, or volume label syntax is incorrect',
              -2147024894: 'The system cannot find the file specified',
              -2147024891: 'Access denied',
              -2147216615: 'Required element or attribute missing',
              -2147216616: 'Value incorrectly formatted or out of range',
              -2147352571: 'Type mismatch'}
        try:
            failure_code = fc[exc[5]]
        except KeyError:
            failure_code = 'Unknown Failure: {0}'.format(error)

        logging.error('Failed to create/register/modify task {0}: {1}'.format(taskName, failure_code))
        return None


DEFAULT_NETWORK_SERVICE_PRIVS = ["SeAssignPrimaryTokenPrivilege",
                                "SeIncreaseQuotaPrivilege",
                                "SeShutdownPrivilege",
                                "SeAuditPrivilege",
                                "SeChangeNotifyPrivilege",
                                "SeUndockPrivilege",
                                "SeImpersonatePrivilege",
                                "SeCreateGlobalPrivilege",
                                "SeIncreaseWorkingSetPrivilege",
                                "SeTimeZonePrivilege"]

DEFAULT_LOCAL_SERVICE_PRIVS = DEFAULT_NETWORK_SERVICE_PRIVS + ["SeSystemtimePrivilege"]